import * as path from 'path';
import * as fs from 'fs';
import * as net from 'net';
import * as _ from 'lodash';
import * as is from 'is-type-of';
import Tracker from './tracker';
import * as helpers from './helpers';
import protocol from './protocol';
import { Logger } from '@nestjs/common';

interface TrackerConfig {
	host: string;
	port: number;
	timeout?: number;
	charset?: string;
}

interface FdfsClientConfig {
	charset?: string;
	trackers: TrackerConfig[];
	timeout?: number;
	defaultExt?: string;
	logger?: any;
}

interface UploadOptions {
	group?: string;
	size?: number;
	ext?: string;
	fileId?: string;
	method?: string;
}

interface DownloadOptions {
	target: string | fs.WriteStream;
	offset?: number;
	bytes?: number;
}

class FdfsClient {
	private readonly logger = new Logger(FdfsClient.name);

	config: FdfsClientConfig;
	_trackerIndex?: number;

	constructor(config: FdfsClientConfig) {
		if (config && config.logger) {
			this.logger.log(config.logger);
		}
		this.config = _.extend({}, defaults, config);
		this._checkConfig();
	}

	_checkConfig() {
		if (!this.config.trackers) {
			throw new Error('you must specify "trackers" in config.');
		}

		if (!Array.isArray(this.config.trackers)) {
			this.config.trackers = [this.config.trackers];
		}

		if (this.config.trackers.length === 0) {
			throw new Error('"trackers" in config is empty.');
		}

		this.config.trackers.forEach(tracker => {
			if (!tracker.host || !tracker.port) {
				throw new Error('"trackers" in config is invalid, every tracker must all have "host" and "port".');
			}
		});
	}

	_getTracker(): Promise<Tracker> {
		if (!this._trackerIndex) {
			this._trackerIndex = 0;
		} else {
			this._trackerIndex++;
			if (this._trackerIndex >= this.config.trackers.length) {
				this._trackerIndex = 0;
			}
		}
		const _self = this;
		let _tracker: Tracker | undefined;
		let config = this._getTrackerConfig(this._trackerIndex);
		return _getTrackerConnection(config).then(async tracker => {
			if (tracker) {
				_tracker = tracker;
				return tracker;
			}
			const tryConnect = async (index: number): Promise<Tracker | undefined> => {
				if (_tracker) {
					return _tracker;
				}
				if (index === _self._trackerIndex) {
					return undefined;
				}
				config = _self._getTrackerConfig(index);
				const tr = await _getTrackerConnection(config);
				if (tr) {
					_tracker = tr;
				}
				return _tracker;
			};

			for (let i = 0; i < _self.config.trackers.length; i++) {
				await tryConnect(i);
				if (_tracker) {
					break;
				}
			}
		}).then(() => {
			if (_tracker) {
				return _tracker;
			}
			throw new Error('all trackers connect fail, please check your tracker config or your tracker server.');
		});
	}

	_getTrackerConfig(index: number): TrackerConfig {
		const config = this.config.trackers[index];
		config.timeout = this.config.timeout;
		config.charset = this.config.charset;
		return config;
	}

	upload(file: string | Buffer | fs.ReadStream | any, options: UploadOptions = {}): Promise<any> {
		const _self = this;
		return _normalizeUploadParams(file, options).then(() => {
			if (!options.ext) {
				options.ext = _self.config.defaultExt;
			}

			if (!options.group && options.fileId) {
				const gf = helpers.id2gf(options.fileId);
				options.group = gf.group;
			}
			return _self._getTracker();
		}).then(tracker => {
			return tracker.getStoreStorage(options.group);
		}).then(storage => {
			return storage.upload(file, options as any);
		});
	}

	download(fileId: string, options: DownloadOptions): Promise<any> {
		const _self = this;
		return new Promise<void>((resolve, reject) => {
			if (!options || _.isFunction(options)) {
				reject(new Error('options.target is not specified'));
				return;
			}

			if (!options.target) {
				const ori = options;
				options = { target: ori } as any;
			}

			if (!(_.isString(options.target) || _.isFunction((options.target as any).write))) {
				reject(new Error('options.target is invalid, it\'s type must be String or WritableStream'));
				return;
			}

			if (_.isString(options.target)) {
				options.target = fs.createWriteStream(options.target);
			}
			resolve();
		}).then(() => {
			return _self._getTracker();
		}).then(tracker => {
			return tracker.getFetchStorage(fileId);
		}).then(storage => {
			return storage.download(fileId, options);
		});
	}

	del(fileId: string): Promise<any> {
		return this._getTracker().then(tracker => {
			return tracker.getUpdateStorage(fileId);
		}).then(storage => {
			return storage.del(fileId);
		});
	}

	remove = this.del;

	setMetaData(fileId: string, metaData: Record<string, any>, flag: string): Promise<any> {
		return this._getTracker().then(tracker => {
			return tracker.getUpdateStorage(fileId);
		}).then(storage => {
			return storage.setMetaData(fileId, metaData, flag);
		});
	}

	getMetaData(fileId: string): Promise<any> {
		return this._getTracker().then(tracker => {
			return tracker.getUpdateStorage(fileId);
		}).then(storage => {
			return storage.getMetaData(fileId);
		});
	}

	getFileInfo(fileId: string): Promise<any> {
		return this._getTracker().then(tracker => {
			return tracker.getUpdateStorage(fileId);
		}).then(storage => {
			return storage.getFileInfo(fileId);
		});
	}

	listGroups(): Promise<any> {
		return this._getTracker().then(tracker => {
			return tracker.listGroups();
		});
	}

	listStorages(group: string): Promise<any> {
		return this._getTracker().then(tracker => {
			return tracker.listStorages(group);
		});
	}
}

const defaults: FdfsClientConfig = {
	charset: 'utf8',
	trackers: [],
	timeout: 10000,
	defaultExt: ''
};

function _getTrackerConnection(config: TrackerConfig): Promise<Tracker | undefined> {
	const logger = new Logger(_getTrackerConnection.name);
	return new Promise((resolve, reject) => {
		const _name = `${config.host}:${config.port}`;
		const socket = new net.Socket();
		logger.debug('connect to tracker server [%s]', _name);
		socket.setTimeout(config.timeout);
		socket.connect(config.port, config.host);

		socket.on('error', err => {
			logger.error('connect to tracker server [' + _name + '] err:', err);
			resolve(undefined);
		});

		socket.on('timeout', () => {
			socket.destroy();
			logger.error('connect to tracker server [' + _name + '] timeout.');
			resolve(undefined);
		});

		socket.on('connect', () => {
			logger.debug('tracker server [%s] is connected', _name);
			resolve(new Tracker(config, socket));
		});
	});
}

function _normalizeUploadParams(file: string | Buffer | fs.ReadStream | any, options: UploadOptions | any): Promise<void> {
	return new Promise<void>((resolve, reject) => {
		if (!file) {
			reject(new Error('The "file" parameter is empty.'));
			return;
		}

		if (!(_.isString(file) || _.isBuffer(file) || _.isFunction((file as any).pipe))) {
			reject(new Error('The "file" parameter is invalid, it must be a String, Buffer, or ReadableStream'));
			return;
		}

		if (_.isFunction((file as any).pipe) && !options.size) {
			reject(new Error('when the "file" parameter\'s is ReadableStream, options.size must specified'));
			return;
		}

		if (_.isBuffer(file)) {
			options.size = file.length;
		}

		if (_.isString(options.size)) {
			options.size = Number(options.size);
		}

		if (options.method === protocol.FDFS_METHOD_UPLOAD_APPENDER_FILE) {
		} else if (options.method === protocol.FDFS_METHOD_APPEND_FILE) {
			if (!options.fileId) {
				reject(new Error('options.fileId is missed'));
			}
		} else if (options.method === protocol.FDFS_METHOD_MODIFY_FILE) {
			if (!options.fileId) {
				reject(new Error('options.fileId is missed'));
			}

			if (_.isString(options.offset)) {
				options.offset = Number(options.offset);
			}

			if (!(options.offset || options.offset === 0)) {
				reject(new Error('options.offset is missed'));
			}
			if (!_.isNumber(options.offset)) {
				reject(new Error('options.offset must be a number'));
			}
		}

		if (_.isString(file)) {
			const stats = fs.statSync(file);
			options.size = stats.size;
			if (!options.ext) {
				options.ext = path.extname(file);
				if (options.ext) {
					options.ext = options.ext.substring(1);
				}
			}
			resolve();
		}
		resolve();
	});
}

export default FdfsClient;
